/**
 * 
 */
package com.novartis.nims.imagedc.logic;

import java.util.HashMap;
import java.util.Map;

import org.apache.oro.text.GlobCompiler;
import org.apache.oro.text.regex.MalformedPatternException;
import org.apache.oro.text.regex.Perl5Matcher;
import org.simpleframework.xml.Element;
import org.simpleframework.xml.ElementMap;
import org.simpleframework.xml.Root;
import org.simpleframework.xml.core.PersistenceException;
import org.simpleframework.xml.core.Validate;


/**
 * Contains a scan name and the criteria used to determine whether that name
 * should be applied to a {@link Scan}. The criteria consist of a set of
 * predicates which can be tested against a Scan. Each predicate refers
 * to a DICOM attribute tag, and the pattern against which its value should
 * be compared when testing the predicate. The pattern is matched using
 * {@link org.apache.oro.text.GlobCompiler}, which documents the glob expression
 * syntax used. Briefly, the * and ? wildcards are supported. If all criteria
 * are satisfied for the attribute values provided by a given Scan, then the
 * Scan matches the Rule.
 * <p>
 * It is the client's responsibility to determine which Scan objects to test
 * against which Rules, and what actions to take on a successful match (for
 * example, creating an association between the Scan and the scan name defined
 * by the matching Rule).
 * <p>
 * This class contains annotations from the Simple framework which define how
 * its instances can be serialized to and deserialized from XML documents.
 * 
 * @author Josh Snyder
 * @see Scan
 * @see RuleSet
 * @see org.apache.oro.text.GlobCompiler
 * @see org.simpleframework.xml
 */
@Root(name="Rule")
public class Rule implements Comparable<Rule> {
	
	//delegate to the oro Pattern class. equals and hashCode are overridden
	//so that it can be profitably used in a Map in place of the oro Pattern
	private class Pattern {
		
		private final org.apache.oro.text.regex.Pattern pattern;
		
		Pattern(org.apache.oro.text.regex.Pattern thePattern) {
			pattern = thePattern;
		}

		@Override
		public boolean equals(Object obj) {
			if (obj instanceof Pattern) {
				return getPatternString().equals(((Pattern)obj).getPatternString());
			} else {
				return false;
			}
		}

		@Override
		public int hashCode() {
			return getPatternString().hashCode();
		}

		public org.apache.oro.text.regex.Pattern getBackingPattern() {
			return pattern;
		}

		private String getPatternString() {
			return pattern.getPattern();
		}
		
	}

	//would declare this final if not for the Simple serialization support
	@ElementMap(name="MatchingCriteria", key="DicomAttribute", value="MatchingPattern", entry="Entry", data=true)
	private Map<AttributeTagDelegate, String> uncheckedCriteria;
	
	private Map<AttributeTagDelegate, Pattern> criteria;
	
	//TODO: since scan name does not affect rule identity, the association could be maintained externally
	//would declare this final if not for the Simple serialization support
	@Element(name="ScanName")
	private String scanName;

	/**
	 *  Constructs the object without initializing its fields. Needed by Simple
	 *  framework to instantiate this class. The access level is private so
	 *  that no clients can instantiate it without subverting access control.
	 */
	@SuppressWarnings("unused")
	private Rule() {}

	/**
	 * Criteria and scan name must be provided at instantiation and cannot be
	 * modified later.
	 * 
	 * @param theCriteria
	 * @param theScanName
	 * @throws MalformedPatternException 
	 */
	public Rule(Map<AttributeTagDelegate, String> theCriteria, String theScanName) throws MalformedPatternException {
		initialize(theCriteria);
		scanName = theScanName;
	}
	
	private void initialize(Map<AttributeTagDelegate, String> theCriteria) throws MalformedPatternException {
		criteria = new HashMap<AttributeTagDelegate, Pattern>();
		
		GlobCompiler compiler = new GlobCompiler();
		for (AttributeTagDelegate tag : theCriteria.keySet()) {
			Pattern pattern = new Pattern(compiler.compile(theCriteria.get(tag)));
			criteria.put(tag, pattern);
		}

		//will no longer be used. dereference to save memory
		uncheckedCriteria = null;
	}
	
	@SuppressWarnings("unused")
	@Validate
	private void initialize() throws PersistenceException {
		try {
			initialize(uncheckedCriteria);
		} catch (MalformedPatternException e) {
			throw new PersistenceException(e, "Invalid matching pattern has been specified.");
		}
	}
	
	/**
	 * Indicates whether some other object is "equal to" this one.
	 * <p>
	 * Another object is considered equal to this one if it is an <code>
	 * instanceof</code> Rule and if it has the same set of criteria.
	 * Therefore Rules with identical criteria but different scan names are
	 * still considered equal.
	 * <p>
	 * Subclasses must be careful to preserve symmetry when overriding
	 * this method.
	 * 
	 * @param obj the reference object with which to compare
	 * @return true if the object is a Rule and has the same criteria.
	 * Otherwise, false.
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (obj instanceof Rule) {
			return criteria.equals(((Rule)obj).criteria);
		} else {
			return false;
		}
	}
	
	/**
	 * Returns the hash code value for this Rule. The hash code is defined as
	 * the hash code of the Map which contains the criteria, since this is
	 * the sole field which determines equality.
	 * 
	 * @return hash code of the criteria map
	 * @see java.util.Map#hashCode()
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		return criteria.hashCode();
	}

	/**
	 * Defines the natural ordering of objects of this class such that those
	 * with fewer predicates in their criteria occur after those with more.
	 * <p>
	 * This method is consistent with equals so that these
	 * objects can be used in SortedSets. Therefore objects with the same
	 * number of predicates aren't necessarily equal with respect to ordering.
	 * They are only equal if the set of predicates is the same for both
	 * objects. If the set is different, but the number is the same, then the
	 * relative ordering is arbitrary. 
	 * 
	 * @return a negative integer, zero, or a positive integer as this object is
	 * less than, equal to, or greater than the specified object
	 * @see java.lang.Comparable#compareTo(java.lang.Object)
	 */
	public int compareTo(Rule rule) {
		if (equals(rule)) {
			//compareTo equality must be consistent with equals equality for this
			//to be correctly used in a SortedSet
			return 0;
		} else if (getPriority() < rule.getPriority()) {
			return 1;
		} else {
			//Rules with the same Priority get an arbitrary comparison result
			return -1;
		}
	}
	
	private int getPriority() {
		return criteria.size();
	}
	
	/**
	 * Determines whether a given {@link Scan} matches this Rule. Each
	 * predicate must evaluate as true for the Scan to match. A predicate
	 * evaluates as true when the Scan's value for the given attribute
	 * tag matches the given pattern.
	 * 
	 * @param scan
	 * @return true if the Scan matches, otherwise false
	 */
	public boolean match(Scan scan) {
		if (criteria.size() < 1) {
			return false;
		}
		
		boolean match = true;
		for (AttributeTagDelegate tag : criteria.keySet()) {
			String value = scan.getAttributeValue(tag);
			if (! valuesMatch(value, criteria.get(tag))) {
				match = false;
				break;
			}
		}
		return match;
	}

	private boolean valuesMatch(String value, Pattern pattern) {
		if (value == null || pattern == null) {
			return false;
		}
		
		Perl5Matcher matcher = new Perl5Matcher();
		return matcher.matches(value, pattern.getBackingPattern());
	}

	/**
	 * Gets the scan name with which the criteria are associated.
	 * 
	 * @return the scan name
	 */
	public String getScanName() {
		return scanName;
	}

}
